package org.jeecg.modules.edu.ca.gap.concept;

import cn.hutool.core.util.ArrayUtil;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.jeecg.modules.edu.ca.entity.CaClassroom;
import org.jeecg.modules.edu.ca.entity.CaTeachingPlan;
import org.jeecg.modules.edu.ca.gap.core.*;
import org.jeecg.modules.edu.ca.gap.entity.*;
import org.jeecg.modules.edu.ca.gap.util.Constant;
import org.jgap.FitnessFunction;
import org.jgap.IChromosome;

import java.util.*;
import java.util.stream.Collectors;

/**
 * 避免过早收敛的解决方案：适值训练
 * 设 `f` 为当前适值, `fmax`为种群最大适值, `favg` 为种群平均适值
 *
 * 重演适值(FitnessFunction): F = (fmax - f) / (fmax - favg) * f
 */
public class CaFitnessFunction extends FitnessFunction {

    private int geneSize;

    public CaFitnessFunction(int geneSize) {
        this.geneSize = geneSize;
    }

    /** 解组染色体 */
    private CaSuperGene[] extractChromosome(IChromosome a_subject , int chromosomeSize) {
        CaSuperGene[] arr = new CaSuperGene[chromosomeSize];
        for(int i=0;i<chromosomeSize;i++){
            arr[i] = (CaSuperGene)a_subject.getGene(i);
        }
        return arr;
    }

    @Override
    protected double evaluate(IChromosome a_subject) {


        /** 惩罚点 */
        double penalty = 0;
        CaGene classroomGene;
        CaGene timeGene;

        TeachingPlan plan;
        CaClassroom classroom;
        Time time;

        // Extract supergenes from chromosome
        CaSuperGene[] s = this.extractChromosome(a_subject, this.geneSize);
        Map<String,Integer> map = new HashMap<>();
        List<Curriculum> curriculumList = new ArrayList<>();
        // -----------------------Checking hard constraints---------------------------
        // -----------种群要求满足：每个课时都要
        for (int i = 0;i < this.geneSize;i++) {
            classroomGene = (CaGene) s[i].geneAt(Constant.CLASSROOM);
            timeGene = (CaGene)s[i].geneAt(Constant.TIME);

            plan = Constant.PLAN_LIST.get(i);
            classroom = Constant.CLASSROOM_LIST.get(classroomGene.getNumber());
            time = Constant.TIME_LIST.get(timeGene.getNumber());
            int weekOrder = time.getWeekOrder();
            int courseOrder = time.getCourseOrder();

            //------教室座位数 > 班级学生数
            if (classroom.getSize() < plan.getClasssSize()){
                penalty += 1000;
            }
            //检查教室类型的合理度
            if (!classroom.getType().equals(plan.getClassroomType())){
                penalty += 1000;
            }

            //检查时间安排合理度
            penalty += this.calculateTimePenalty(
                    weekOrder,
                    courseOrder,
                    plan.getFirstTime(),
                    plan.getSecondTime(),
                    plan.getThirdTime(),
                    plan.getFourthTime()
            );

            //构造map，用于后面检测冲突
            String classsKey = "classs:" + weekOrder+":"+courseOrder+":"+plan.getClasss();
            Integer classsValue = map.get(classsKey);
            if(classsValue !=null){
                map.put(classsKey,classsValue + 1);
            }else{
                map.put(classsKey,0);
            }
            String teacherKey = "teacher:" + weekOrder+":"+courseOrder+":"+plan.getTeacher();
            Integer teacherValue = map.get(teacherKey);
            if(teacherValue !=null){
                map.put(teacherKey,teacherValue + 1);
            }else{
                map.put(teacherKey,0);
            }
            String classroomKey = "classroom:" + weekOrder+":"+courseOrder+":"+classroom.getId();
            Integer classroomValue = map.get(classroomKey);
            if(classroomValue !=null){
                map.put(classroomKey,classroomValue + 1);
            }else{
                map.put(classroomKey,0);
            }
            //构造临时数据，用于后面检测冲突
            curriculumList.add(new Curriculum()
                    .setClasss(plan.getClasss())
                    .setCourse(plan.getCourse())
                    .setTeacher(plan.getTeacher())
                    .setClassroom(classroom.getId())
                    .setWeekOrder(time.getWeekOrder())
                    .setCourseOrder(time.getCourseOrder())
            );
        }
        //-----检查时间是否冲突
        for (Map.Entry<String,Integer> entry:map.entrySet()) {
            //同一时间，出现相同班级，或者相同教师，或者相同的教室
            if(entry.getValue() != 0){
                penalty += 1000;
            }
        }
        // 课程离散程度
        penalty += this.calculateDiscretePenalty(curriculumList);

        return 1 / (1 + penalty);
    }

    /**
     * 计算时间与期望的时间之间的差距
     * @param weekOrder
     * @param courseOrder
     * @param first
     * @param second
     * @param third
     * @param fourth
     * @return
     */
    private int calculateTimePenalty( int weekOrder,int courseOrder,String first,String second,String third,String fourth){
        if(StringUtils.isNotBlank(first)){
            String[] firstArr = first.split(",");
            if(ArrayUtil.indexOf(firstArr,courseOrder+"") != -1 ){
                return 0;
            }
        }
        if(StringUtils.isNotBlank(first)){
            String[] secondArr = second.split(",");
            if(ArrayUtil.indexOf(secondArr,courseOrder+"") != -1 ){
                return 2;
            }
        }
        if(StringUtils.isNotBlank(first)){
            String[] thirdArr = third.split(",");
            if(ArrayUtil.indexOf(thirdArr,courseOrder+"") != -1 ){
                return 4;
            }
        }
        if(StringUtils.isNotBlank(first)){
            String[] fourthArr = fourth.split(",");
            if(ArrayUtil.indexOf(fourthArr,courseOrder+"") != -1 ){
                return 8;
            }
        }
        return 10;
    }

    /**
     * 计算课程离散度惩罚值
     * @param list
     * @return
     */
    private double calculateDiscretePenalty(List<Curriculum> list) {
        // 离散程度期望值
        int penalty = 0;
        //将班级所有染色体按课程编码进行分组
        Map<String,List<Curriculum>> cmap = list.stream()
                .collect(Collectors.groupingBy(c -> c.getClasss(),Collectors.toList()));

        //按课程编码分组保存课程编号，即该班级的每种课程都在哪几节课上，并排序
        for (Map.Entry<String,List<Curriculum>> entry:cmap.entrySet()){
            if(entry.getValue().size() > 1) {
                // 返回课程的上课时间先按周几，再按第几节进行排序
                List<Curriculum> curriculumList = entry.getValue().stream().sorted(
                        Comparator.comparing(Curriculum::getWeekOrder).thenComparing(Curriculum::getCourseOrder)
                ).collect(Collectors.toList());

                for (int i = 0; i < curriculumList.size() - 1; ++i) {
                    Curriculum curr = curriculumList.get(i);
                    Curriculum next = curriculumList.get(++i);
                    // 计算一门课上课的时间差
                    int difference = (next.getWeekOrder()-curr.getWeekOrder()) * Constant.COURSE_NUM_BY_DAY + next.getCourseOrder() - curr.getCourseOrder();
                    penalty += this.judgingDiscreteValues(difference);
                }
            }
        }
        return penalty;
    }

    /**
     * 判断两课程的时间差在哪个区间,并返回对应的惩罚值
     * @param difference
     * @return
     */
    private int judgingDiscreteValues(int difference) {
        //每天5节课，每周上5天，共25节课，其时间差期望值如下
        //int[] tenExpectValue = {5, 6, 7, 8}; // 期望值为10时两课之间的时间差
        //int[] sixExpectValue = {4, 9, 10, 11, 12, 13}; // 期望值为6时两课之间的时间差
        //int[] fourExpectValue = {3, 14, 15, 16, 17, 18}; // 期望值为4时两课之间的时间差
        //int[] twoExpectValue = {2, 19, 20, 21, 22, 23}; // 期望值为2时两课之间的时间差
        //int [] zeroExpectValue = {1,24};//期望值为0时两课之间的时间差

        //根据如上可得出如下公式
        if (difference >= Constant.COURSE_NUM_BY_DAY && difference <= Constant.COURSE_NUM_BY_DAY * 2 - 2) {
            return 0;
        } else if (difference == Constant.COURSE_NUM_BY_DAY - 1 || (difference >= Constant.COURSE_NUM_BY_DAY * 2 - 1 && difference <= Constant.COURSE_NUM_BY_DAY * 3 - 2)) {
            return 2;
        } else if (difference == Constant.COURSE_NUM_BY_DAY - 2 || (difference >= Constant.COURSE_NUM_BY_DAY * 3 - 1 && difference <= Constant.COURSE_NUM_BY_DAY * 4 - 2)) {
            return 4;
        } else if (difference == Constant.COURSE_NUM_BY_DAY - 3 || (difference >= Constant.COURSE_NUM_BY_DAY * 4 - 1 && difference <= Constant.COURSE_NUM_BY_DAY * 5 - 2)) {
            return 6;
        } else {
            return 10;
        }
    }
}
